(function($) {

  /**
   *  Default options:
   *
   *    - lazy
   *      If true, show a static preview with a 'click to load' button to load
   *      the 3D preview.
   *
   *    - rotations:
   *      When creating pseudo-3d previews, number of frames per rotation.
   *
   *    - imageonly:
   *      Create a preview that is an image, even if there is an alternate
   *      rich-media display option.
   *
   *    - sizex, sizey:
   *      When creating image or pseudo3d previews, use this image size.
   */
  var defaults = {
    'lazy'       : false,
    'imageonly'  : false,
    'rotations'  : 12,
    'sizex'      : 256,
    'sizey'      : 256
  };

  var PreviewLoadOrTranscode_url_cache = {};
  var PreviewLoadOrTranscode_data_cache = {};
  var PreviewHandlerRegistry = {};

  // DAMN base URL.
  var DAMNRepository;
  if (window.DAMNRepository) {
    DAMNRepository = window.DAMNRepository;
  }
  else {
    var scripts = document.getElementsByTagName('script');
    var path = scripts[scripts.length-1].src.split('?')[0];      // remove any ?query
    DAMNRepository = path.split('/').slice(0, -4).join('/');
  }

  // DAMN static file base.
  var DAMNStaticFiles;
  if (window.DAMNStaticFiles) {
    DAMNStaticFiles = window.DAMNStaticFiles;
  }
  else {
    DAMNStaticFiles = false;
  }

  // Path templates for transcoding.
  if (window.DAMNPathTemplates) {
    DAMNPathTemplates = window.DAMNPathTemplates;
  }
  else {
    $.ajax({
      url: DAMNRepository + '/transcoders/formats?format=json',
      dataType: 'json',
      type: 'GET',
      async: false,
      cache: true,
      error: function(jqXHR, textStatus, errorThrown) {
        DAMNPathTemplates = {};
      },
      success: function(data) {
        DAMNPathTemplates = data;
      }
    });
  }

  /**
   *  Utility function: return the image src for the broken image icon.
   */
  var PreviewBrokenFileIconSrc = function() {
    // TODO: make it configurable.
    return DAMNRepository + '/static/file_broken.png';
  };

  /**
   *  Utility function: sueastside's magic recipe. No actual human could
   *  possibly read or understand this. Therefore sueastside must be a robot.
   */
  RegExp.escape = function(text) {
    return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
  };

  /**
   *  Utility function: cached asset path rewriting.
   *  @param options Transcoder options as defined by DAMNPathTemplates.
   */
  var _RewriteUrl = function(options) {
    var uuid = options.uuid;
    if (DAMNPathTemplates[options.src_format]) {
      var dsts = DAMNPathTemplates[options.src_format];
      if (dsts[options.format]) {
        var dst = dsts[options.format];
        var path = dst.template.replace(new RegExp(RegExp.escape('${uuid}'), "g"), uuid);
        for (var o in dst.options) {
          var option = dst.options[o];
          var re = new RegExp(RegExp.escape('${'+option.name+'}'), "g");
          if (options[option.name]) {
            path = path.replace(re, options[option.name]);
          }
          else {
            path = path.replace(re, option['default']);
          }
        }
        return path;
      }
    }
    return false;
  };

  /**
   *  Utility function: check if src exists.
   *  @param src URL to check.
   *  @param options Object with options:
   *    * ['type']: 'image' or 'ajax', depending on what is being requested.
   *    * ['callback']: Callback for when the file exists.
   *  @param onerror Callback for when the file does not exist.
   *  @see PreviewLoadOrTranscode
   */
  var _PreviewCheckLoad = function(src, options, onerror) {
    if (options.type == 'image') {
      var testimg = new Image;
      $(testimg).bind('load', function(event) { options.callback(src, options); });
      $(testimg).bind('error', function(event) { onerror('Could not load image: ' + src); });
      testimg.src = src;
    }
    else if (options.type == 'audio') {
      var source = $('<source>').attr({
        'src': src,
        'type': 'audio/ogg; codecs="vorbis"'
      });
      var $testaudio = $('<audio>').append(source).hide();
      $('body').append($testaudio); // TODO: remove from DOM after test
      var testaudio = $testaudio.get(0);
      if (!testaudio.canPlayType) {
        onerror('Your browser does not support the audio tag. Consider <a href="http://www.mozilla.com/en-US/firefox/fx/?from=getfirefox">upgrading it</a>');
        return;
      }
      if (!testaudio.canPlayType('audio/ogg; codecs="vorbis"')) {
        onerror('Your browser does not support the audio/ogg audio type. Consider <a href="http://www.mozilla.com/en-US/firefox/fx/?from=getfirefox">upgrading it</a>');
        return;
      }
      $testaudio.bind('loadeddata', function(event) {
        options.callback(src, options);
      });
      $testaudio.bind('error', function(event) {
        onerror('Could not load audio: ' + src);
      });
      source.bind('error', function(event) {
        onerror('Could not load audio: ' + src);
      });
    }
    else {
      $.ajax({
        url: src,
        type: 'GET',
        cache: true,
        success: function(data) {
          PreviewLoadOrTranscode_data_cache[src] = data;
          options.data = data;
          options.callback(src, options);
        },
        error: function(xhr, ajaxOptions, thrownError) {
          onerror('Could not load XML: ' + xhr.status);
        }
      });
    }
  };

  /**
   *  Utility function: no static file was found so we are transcoding.
   *  @param options Standard options.
   *  @see PreviewLoadOrTranscode
   */
  var _PreviewTranscode = function(options) {
    var src = DAMNRepository;
    src += '/assets/';
    src += options.transcode.uuid;
    src += '/transcode/?';
    for (var option in options.transcode) {
      if (option == 'uuid') {
        continue;
      }
      src += '&'+option+'=' + encodeURIComponent(options.transcode[option]);
    }
    _PreviewCheckLoad(src, options, function(message) {
      if (options.error) {
        options.error(message);
      }
    });
  };

  /**
   *  Core loading function used to load images and XML. Attempts to load a
   *  transcoded file from the static file server if one has been set,
   *  otherwise call the base DAMN server with a transcoder URL.
   *  @param options Array of options:
   *    * 'transcode': Array containing DAMN transcoder options as defined by
   *      DAMNPathTemplates (uuid, format, src_format, sizex, sizey, angley).
   *    * 'callback': Function called back upon success. Required. Signature is
   *      (src, options) where src is the correct path of the resource and
   *      options is the array passed to PreviewLoadOrTranscode.
   *    * 'error': Error function. Signature is (message).
   */
  var PreviewLoadOrTranscode = function(options) {
    if (DAMNStaticFiles) {
      var src = _RewriteUrl(options.transcode);
      if (src) {
        if (PreviewLoadOrTranscode_url_cache[src]) {
          // Only load things once.
          src = DAMNStaticFiles + '/' + src;
          options.data = PreviewLoadOrTranscode_data_cache[src];
          options.callback(src, options);
        }
        else {
          PreviewLoadOrTranscode_url_cache[src] = true;
          _PreviewCheckLoad(DAMNStaticFiles + '/' + src, options, function(message) {
            // Fallback to transcode if no static file is available.
            _PreviewTranscode(options);
          });
        }
      }
      else {
        // No mapping was found, use the transcoder URL.
        _PreviewTranscode(options);
      }
    }
    else {
      // No static file server is configured, use the transcoder URL.
      _PreviewTranscode(options);
    }
  };

  /**
   *  Custom URL resolver for previews.
   */
  var PreviewCSURLResolver = function() { };
  PreviewCSURLResolver.prototype.Resolve = function(assetId, callback) {
    PreviewLoadOrTranscode({
      transcode: {
        uuid: assetId,
        src_format: 'image/png',
        format: 'image/png',
        sizex: 512,
        sizey: 512,
        angley: 0.0
      },
      type: 'image',
      callback: callback
    });
    // TODO: error callback, put the UV mapper image.
  };

  /**
   *  Construct a handler for the specified type, pass the data model to it,
   *  and call the Init() function.
   *  @param type High level asset type such as 'Image' or '3D Mesh'.
   *  @param model Data model.
   *  @param parent Parent / wrapper HTML element (JQuery object).
   *  @return Newly constructed PreviewHandler object.
   */
  function CreatePreviewHandler(type, model, parent) {
    if (type && PreviewHandlerRegistry[type]) {
      // A type was specified and we have a handler for it.
      var handler = eval('new ' + PreviewHandlerRegistry[type] + '();');
      handler.model = model;
      handler.$parent = parent;
      handler.parent = parent.get(0);
      handler.Init();
      return handler;
    }
    else {
      // Unknown type was passed, use the ambiguous handler.
      // We know 'None' is defined, directly below this code.
      return CreatePreviewHandler('None', model, parent);
    }
  }

  /**
   *  Add a new type of handler to the preview system.
   *  @param type High level asset type such as 'Image' or '3D Mesh'.
   *  @param constructor Name of the class / constructor function that
   *         implements previews for the given high level type.
   */
  function AddPreviewHandler(type, constructor) {
    PreviewHandlerRegistry[type] = constructor;
  }

  // Built-in system handlers.
  AddPreviewHandler('3D Mesh', 'MeshHandler');
  AddPreviewHandler('CSMeshHandler', 'CSMeshHandler');
  AddPreviewHandler('Pseudo3DMeshHandler', 'Pseudo3DMeshHandler');
  AddPreviewHandler('Image', 'ImageHandler');
  AddPreviewHandler('Audio', 'AudioHandler');
  AddPreviewHandler('None', 'AmbiguousHandler');

  /**
   *  Base class for PreviewHandler.controller.
   *  @param handler PreviewHandler that this is a controller for.
   */
  function BaseController(handler) {
    this.handler = handler;
    this.idGen = 0;
    this.controllerCallbacks = {
      'OnBind': [],
      'OnUnbind': []
    };
    this.eventInfoCache = {};
  }

  /**
   *  Extend the controller, add support for new event types.
   *  @param  base Class to extend this controller with.
   *  @return Returns `this` object for chained construction.
   */
  BaseController.prototype.Extend = function(base) {
    var baseinstance = new base();
    for (var callback in this.controllerCallbacks) {
      var testcallbacks = [ callback,  '_' + callback,  '__' + callback ];
      for (var i in testcallbacks) {
        var testcallback = testcallbacks[i];
        for (var basefunc in baseinstance) {
          if (typeof(baseinstance[basefunc]) != 'function') {
            continue;
          }
          if (basefunc.substring(0, testcallback.length) == testcallback) {
            this.idGen++;
            var newname = callback + '_' + this.idGen;
            baseinstance[newname] = baseinstance[basefunc];
            delete baseinstance[basefunc];
            this.controllerCallbacks[callback][this.controllerCallbacks[callback].length] = newname;
          }
        }
      }
    }
    $.extend(this, baseinstance);
    return this;
  };

  /**
   *  Internal utility function: call back the 'OnBind' or 'OnUnbind' functions
   *  of controller extension classes added with Extend().
   *  @param  base Class to extend this controller with.
   *  @return Returns `this` object for chained construction.
   */
  BaseController.prototype.ControllerCallback = function(name, eventinfo) {
    var rval = true;
    for (var index in this.controllerCallbacks[name]) {
      var callback = this.controllerCallbacks[name][index];
      if (this[callback](eventinfo) === false) {
        rval = false;
      }
    }
    return rval;
  };

  /**
   *  Bind event callback(s) to the preview parent DOM element. Note: always
   *  use a namespace suffix in event bindings. It allows cleaner unbinding.
   *  @param callbacks Associative array of event callbacks. Example: {
   *    'mousedown.MyClickHandler': function(event) {
   *      // ...
   *    },
   *    'mouseup.MyClickHandler': function(event) {
   *      // ...
   *    }
   *  }
   *  @return Returns `this` object for chained construction.
   */
  BaseController.prototype.Bind = function(callbacks) {
    var self = this;
    for (var eventname in callbacks) {
      var eventns = eventname.split('.');
      var eventtype = eventns[0];
      var namespace = eventns[1];
      var eventinfo = {
        'callbacks': callbacks,
        'eventname': eventname,
        'eventtype': eventtype,
        'namespace': namespace
      };
      this.eventInfoCache[eventname] = eventinfo;
      if (self.ControllerCallback('OnBind', eventinfo)) {
        self.handler.$parent.bind(eventname, function(e) {
          e.handler = self.handler;
          callbacks[eventname](e);
        });
      }
    }
    return self;
  };

  /**
   *  Unbind an event callback from the preview parent DOM element.
   *  @param callback Name of the callback, for example 'mousedown.MyClickHandler'.
   *  @return Returns `this` object for chained construction.
   */
  BaseController.prototype.Unbind = function(eventname) {
    if (this.ControllerCallback('OnUnbind', this.eventInfoCache[eventname])) {
      this.handler.$parent.unbind(eventname);
    }
    return this;
  };

  /**
   *  Controller extension classes should define this function to take action
   *  when event listeners are bound to a DOM element. Controller extension
   *  classes may also declare _OnBind, __OnBind, or any function where the
   *  name begins with OnBind, _OnBind, or __OnBind. All such named functions
   *  are called when the controller binds. __OnBind_DOM is a reserved name.
   *  @param eventinfo Event information structure.
   *  @see BaseController.Bind
   */
  BaseController.prototype.OnBind = function(eventinfo) { };

  /**
   *  Controller extension classes should define this function to take action
   *  when event listeners are unbound from a DOM element. Controller extension
   *  classes may also declare _OnUnbind, __OnUnbind, or any function where the
   *  name begins with OnUnbind, _OnUnbind, or __OnUnbind. All such named
   *  functions are called when the controller unbinds. __OnUnbind_DOM is a
   *  reserved name.
   *  @param eventinfo Event information structure.
   *  @see BaseController.Unbind
   */
  BaseController.prototype.OnUnbind = function(eventinfo) { };

  /** Utility function: Fire an event. Used for custom event types that are not
   *  fired by the browser.
   *  @param eventtype Type of event such as 'click'.
   *  @param eventinfo Event info structure passed to OnBind() and OnUnbind().
   *  @param event Object representing the what happened.
   *  @return Returns false if any callback returned false, true otherwise.
   */
  BaseController.prototype.FireEvent = function(eventtype, eventinfo, event) {
    var rval = true;
    for (var callback in eventinfo.callbacks) {
      var suffixed = eventtype + '.';
      if (callback == eventtype || callback.substring(0, suffixed.length) == suffixed) {
        event.previewtarget = this.handler; // TODO: get previewtarget on DOM events too.
        var result = eventinfo.callbacks[callback](event);
        if (result === false) {
          rval = false;
        }
      }
    }
    return rval;
  };

  /**
   *  Namespace for internal controllers.
   */
  var Controllers = { };

  /**
   *  Built-in DOM event controllers.
   */
  var DOMEvents = ['blur', 'focus', 'focusin', 'focusout', 'load', 'resize', 'scroll', 'unload', 'click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave', 'change', 'select', 'submit', 'keydown', 'keypress', 'keyup', 'error'];
  // TODO: can we get this list off the browser or JQuery somehow?
  for (var i in DOMEvents) {
    var type = DOMEvents[i];
    Controllers[type] = function() { };
    $.extend(Controllers[type].prototype, BaseController.prototype);
    Controllers[type].prototype.__OnBind_DOM = function(eventinfo) {
      var self = this;
      if (eventinfo.eventtype != type) {
        return;
      }
      for (var eventname in eventinfo.callbacks) {
        if (eventname.substring(0, type.length) == type) {
          self.handler.$parent.bind(eventname, function(event) {
            self.FireEvent(type, eventinfo, event);
          });
        }
      }
    };
    Controllers[type].prototype.__OnUnbind_DOM = function(eventinfo) {
      if (eventinfo.eventtype != type) {
        return;
      }
      for (var eventname in eventinfo.callbacks) {
        if (eventname.substring(0, type.length) == type) {
          this.handler.$parent.unbind(eventname);
        }
      }
    };
  }

  /**
   *  When a click event is bound, add class 'clickable' to the parent.
   *  @param eventinfo Event information structure.
   *  @see BaseController.OnBind
   */
  Controllers.click.prototype.OnBind = function(eventinfo) {
    if (eventinfo.eventtype == 'click') {
      this.handler.$parent.addClass('clickable');
    }
  };

  /**
   *  When a click event is unbound, remove class 'clickable' from the parent.
   *  @param eventinfo Event information structure.
   *  @see BaseController.OnUnbind
   */
  Controllers.click.prototype.OnUnbind = function(eventinfo) {
    if (eventinfo.eventtype == 'click') {
      this.handler.$parent.removeClass('clickable');
    }
  };

  /**
   *  Scroll wheel events.
   */
  Controllers.Wheel = function() { };
  $.extend(Controllers.Wheel.prototype, BaseController.prototype);

  /**
   *  Bind events for scroll wheel operation.
   *  @param eventinfo Event information structure.
   *  @see BaseController.OnBind
   */
  Controllers.Wheel.prototype.OnBind = function(eventinfo) {
    var self = this;
    var callback = function(event) {
      event.wheel = event.detail ? event.detail : event.wheelDelta;
      self.FireEvent('wheel', eventinfo, event);
    };
    if (eventinfo.eventtype == 'wheel') {
      if (self.handler.parent.addEventListener) {
        self.handler.parent.addEventListener('DOMMouseScroll', callback, false);
      }
      else if (self.handler.parent.attachEvent) {
        self.handler.parent.attachEvent("onmousewheel", callback);
      }
    }
  };

  /**
   *  Unbind events for scroll wheel operation.
   *  @param eventinfo Event information structure.
   *  @see BaseController.OnUnbind
   */
  Controllers.Wheel.prototype.OnUnbind = function(eventinfo) {
    self.handler.parent.removeEventListener('DOMMouseScroll')
  }

  /**
   *  Interactive drag control, the content of the preview is going to change
   *  somehow based on click / drag user input.
   */
  Controllers.InteractiveDrag = function() { };
  $.extend(Controllers.InteractiveDrag.prototype, BaseController.prototype);

  /**
   *  Bind events for interactive dragging operation.
   *  @param eventinfo Event information structure.
   *  @see BaseController.OnBind
   */
  Controllers.InteractiveDrag.prototype.OnBind = function(eventinfo) {
    if (eventinfo.eventtype != 'drag' && eventinfo.eventtype != 'drag-start' && eventinfo.eventtype != 'drag-end') {
      return true;
    }
    if (eventinfo.eventtype == 'drag') {
      var self = this;
      self.handler.$parent.addClass('draggable');
      self.handler.$parent.bind('mousedown.InteractiveDrag', function(event) {
        if (event.which > 1) {
          return true;
        }
        self.FireEvent('drag-start', eventinfo, event);
        self.handler.$parent.bind('mousemove.InteractiveDrag', function (event) {
          self.FireEvent('drag', eventinfo, event);
        });
        $(document).bind('mouseup.InteractiveDrag', function (event) {
          self.FireEvent('drag-end', eventinfo, event);
          self.handler.$parent.unbind('mousemove.InteractiveDrag');
          $(document).unbind('mouseup.InteractiveDrag');
        });
        return false;
      });
    }
    return false; // Indicates that BaseController should not try to $.bind().
  };

  /**
   *  Unbind events for interactive dragging operation.
   *  @param eventinfo Event information structure.
   *  @see BaseController.OnUnbind
   */
  Controllers.InteractiveDrag.prototype.OnUnbind = function(eventinfo) {
    if (eventinfo.eventtype != 'drag') {
      return;
    }
    self.handler.$parent.removeClass('draggable');
    self.handler.$parent.unbind('mousedown.InteractiveDrag');
  };

  /**
   *  PreviewHandler constructor. Not normally used directly, except
   *  PreviewHandler.prototype is used to extend the class.
   */
  function PreviewHandler() { }

  /**
   *  Base implementation of Init, call this when Init must be overriden.
   *  @see PreviewHandler.OnInit
   */
  PreviewHandler.prototype.PreviewHandlerInit = function() {
    this.controller = new BaseController(this);
    this.controller.Extend(Controllers.resize);
    this.OnInit();
  };

  /**
   *  Initialization function. By this point, this.model and this.$parent
   *  will have been set by the PreviewHandler construction function.
   *  Subclasses should override PreviewHandler.OnInit() instead of this
   *  function. Called before Add().
   *  @see PreviewHandler.OnInit
   */
  PreviewHandler.prototype.Init = function() {
    this.PreviewHandlerInit();
  };

  /**
   *  Subclasses override this function to take initialization actions that
   *  require access to the model array.
   *  @see PreviewHandler.Init
   */
  PreviewHandler.prototype.OnInit = function() { };

  /**
   *  Resize a preview and allow other code to react.
   *  @param width New preview width.
   *  @param height New preview height.
   */
  PreviewHandler.prototype.Resize = function(width, height) {
    width = parseInt(width, 10);
    height = parseInt(height, 10);
    if (this.$parent) {
      this.$parent.width(width);
      this.$parent.height(height);
    }
    this.OnResize(width, height);
  };

  /**
   *  React to a resize event being invoked on the preview. PreviewHandler
   *  subclasses should reimplement this function to take appropriate action.
   *  @param width New preview width.
   *  @param height New preview height.
   */
  PreviewHandler.prototype.OnResize = function(width, height) { };

  /**
   *  Specfy that an error has occured in the PreviewHandler and allow other
   *  code to react.
   *  @param message Message describing what went wrong.
   */
  PreviewHandler.prototype.Error = function(message) {
    this.$parent.append($("<span class='error'>" + message + "</span>"));
    this.OnError(message);
  };

  /**
   *  React when an error occurred while handling the preview.
   *  @param message Message describing what went wrong.
   */
  PreviewHandler.prototype.OnError = function(message) {
    // TODO: log.
    alert('Preview handler error: ' + message);
  };

  /**
   *  Abstract. Called on subclasses to add preview display element(s) to the
   *  parent container. Called after Init().
   */
  PreviewHandler.prototype.Add = function() { };

  /**
   *  Abstract. Called when the element is being removed. Note: it is not
   *  strictly required to remove elements added to parent in
   *  PreviewHandler.Add(), since parent.empty() will be called, but it is
   *  recommended to do so anyway. This makes it easier for future developers
   *  to modify the actions taken upon removal.
   */
  PreviewHandler.prototype.Remove = function() { };

  /**
   *  Base class for handlers that delegate the actual handling to other
   *  PreviewHandler implementations. Subclasses should override
   *  GetProxyHandler().
   *  @see ProxyHandler.GetProxyHandler
   */
  function ProxyHandler() { }
  $.extend(ProxyHandler.prototype, PreviewHandler.prototype);

  /**
   *  Overrides PreviewHandler.Init(). Calls GetProxyHandler() to retrieve
   *  the correct handler and then invokes Init() on it.
   *  @see PreviewHandler.Init, PreviewHandler.OnInit
   */
  ProxyHandler.prototype.Init = function() {
    this.PreviewHandlerInit();
    this.proxyhandler = this.GetProxyHandler();
  };

  /**
   *  Implements PreviewHandler.OnResize(), calls Resize() on the proxy handler.
   *  @see PreviewHandler.Resize, PreviewHandler.OnResize
   */
  ProxyHandler.prototype.OnResize = function(width, height) {
    this.proxyhandler.Resize(width, height);
  };

  /**
   *  Implements PreviewHandler.OnError(), calls Error() on the proxy handler.
   *  @see PreviewHandler.Error, PreviewHandler.OnError
   */
  ProxyHandler.prototype.OnError = function(message) {
    this.proxyhandler.Error(message);
  };

  /**
   *  Implements PreviewHandler.Add(), calls Add() on the proxy handler.
   *  @see PreviewHandler.Add
   */
  ProxyHandler.prototype.Add = function() {
    this.proxyhandler.Add();
  };

  /**
   *  Implements PreviewHandler.Remove(), calls Remove() on the proxy handler.
   *  @see PreviewHandler.Remove
   */
  ProxyHandler.prototype.Remove = function() {
    this.proxyhandler.Remove();
  };

  /**
   *  Abstract. Subclasses should override this function to specify a
   *  PreviewHandler object to use for a proxy.
   */
  ProxyHandler.prototype.GetProxyHandler = function() {
    return CreatePreviewHandler('Image', this.model, this.$parent);
  };

  /**
   *  Construct an ImageHandler PreviewHandler.
   *  The ImageHandler PreviewHandler can display a preview for any type of
   *  asset and can be used to create thumbnails.
   */
  function ImageHandler() {
    this.$view = $('<img>');
    this.view = this.$view.get(0);
  }
  $.extend(ImageHandler.prototype, PreviewHandler.prototype);

  /**
   *  Implements PreviewHandler.OnResize().
   *  @see PreviewHandler.OnResize
   */
  ImageHandler.prototype.OnResize = function(width, height) {
    // TODO: re-transcode if it is detected that a
    // significant size variation was requested.
    this.$view.width(width);
    this.$view.height(height);
  };

  /**
   *  Implements PreviewHandler.OnError().
   *  @see PreviewHandler.OnError
   */
  ImageHandler.prototype.OnError = function(message) {
    this.$view.attr('src', PreviewBrokenFileIconSrc());
    this.$view.attr('title', message);
  };

  /**
   *  Implements PreviewHandler.Add().
   *  @see PreviewHandler.Add
   */
  ImageHandler.prototype.Add = function() {
    var self = this;
    self.$view.width(self.$parent.width());
    self.$view.height(self.$parent.height());
    self.$parent.append(self.$view);
    PreviewLoadOrTranscode({
      transcode: {
        uuid: self.model.assetid,
        src_format: self.model.src_format,
        format: 'image/png',
        sizex: self.model.sizex,
        sizey: self.model.sizey
      },
      type: 'image',
      callback: function(src, options) {
        self.$view.attr('src', src);
      },
      error: function(msg) {
        self.Error(msg);
      }
    });
  };

  /**
   *  Implements PreviewHandler.Remove().
   *  @see PreviewHandler.Remove
   */
  ImageHandler.prototype.Remove = function() {
    this.$view.remove();
  };

  /**
   *  Construct a CSMeshHandler PreviewHandler.
   *  This PreviewHandler type displays a 3D mesh object using WebGL/CS.
   */
  function CSMeshHandler() { }
  $.extend(CSMeshHandler.prototype, PreviewHandler.prototype);

  /**
   *  Implements PreviewHandler.OnInit().
   *  @see PreviewHandler.OnInit
   */
  CSMeshHandler.prototype.OnInit  = function() {
    var self = this;
    self.$view = $('<canvas>').attr({
      'width': self.$parent.width(),
      'height': self.$parent.height()
    });
    self.view = self.$view.get(0);
    self.lastX = 0.0;
    self.lastY = 0.0;
    self.deltaX = 0.0;
    self.deltaY = 0.0;
    self.mouseMoved = 0;
    self.interval1;
    self.interval2;
    self.controller.Extend(Controllers.InteractiveDrag).Extend(Controllers.Wheel).Bind({
      'wheel.CSMeshHandler': function(event) {
        self.engine.RenderManager.scale += event.wheel / 10;
        self.engine.RenderManager.Frame();
        // TODO: This event handling code is ugly. When we have to use DOM
		// events, let's wrap them like JQuery does.
		if (event.stopPropagation) {
          event.stopPropagation();
		}
        if (event.preventDefault) {
          event.preventDefault();
		}
        event.cancelBubble = true;
        event.cancel = true;
        event.returnValue = false;
      },
      'drag-start.CSMeshHandler': function(event) {
        self.lastX = event.pageX;
        self.lastY = event.pageY;
      },
      'drag.CSMeshHandler': function(event) {
        self.mouseMoved = 1;
        self.deltaX = event.pageX - self.lastX;
        self.deltaX = self.deltaX * - 1;
        self.lastX = event.pageX;
        self.engine.RenderManager.currentRotationX -= self.deltaX;
        self.deltaY = event.pageY - self.lastY;
        self.deltaY *= - 1;
        self.lastY = event.pageY;
        self.engine.RenderManager.currentRotationY -= self.deltaY;
        while (self.engine.RenderManager.currentRotationX < 0) {
          self.engine.RenderManager.currentRotationX += 360;
        }
        if (self.engine.RenderManager.currentRotationX >= 360) {
          self.engine.RenderManager.currentRotationX = self.engine.RenderManager.currentRotationX % 360;
        }
        while (self.engine.RenderManager.currentRotationY < 0) {
          self.engine.RenderManager.currentRotationY += 360;
        }
        if (self.engine.RenderManager.currentRotationY >= 360) {
          self.engine.RenderManager.currentRotationY = self.engine.RenderManager.currentRotationY % 360;
        }
        self.engine.RenderManager.Frame();
        return true;
      },
      'drag-end.CSMeshHandler': function(event) {
        if (self.mouseMoved == 1) {
          var rotationSpeedY = self.deltaY * -1;
          var rotationSpeedX = self.deltaX * -1;
          self.interval1 = setInterval(function(late) {
            if (rotationSpeedX > 0) {
              self.engine.RenderManager.currentRotationX = self.engine.RenderManager.currentRotationX + rotationSpeedX;
              if (rotationSpeedX <= 0) {
                clearInterval(self.interval1);
              }
              rotationSpeedX = rotationSpeedX + -0.2;
              rotationSpeedX = rotationSpeedX * 0.95;
            }
            if (rotationSpeedX < 0) {
              self.engine.RenderManager.currentRotationX = self.engine.RenderManager.currentRotationX + rotationSpeedX;
              if (rotationSpeedX >= 0) {
                clearInterval(self.interval1);
              }
              rotationSpeedX = rotationSpeedX + 0.2;
              rotationSpeedX = rotationSpeedX * 0.95;
            }
            self.engine.RenderManager.Frame();
          }, 20);
          self.interval2 = setInterval(function(late) {
            if (rotationSpeedY > 0) {
              self.engine.RenderManager.currentRotationY = self.engine.RenderManager.currentRotationY + rotationSpeedY;
              if (rotationSpeedY <= 0) {
                clearInterval(self.interval2);
              }
              rotationSpeedY = rotationSpeedY + -0.2;
              rotationSpeedY = rotationSpeedY * 0.95;
            }
            if (rotationSpeedY < 0) {
              self.engine.RenderManager.currentRotationY = self.engine.RenderManager.currentRotationY + rotationSpeedY;
              if (rotationSpeedY >= 0) {
                clearInterval(self.interval2);
              }
              rotationSpeedY = rotationSpeedY + 0.2;
              rotationSpeedY = rotationSpeedY * 0.95;
            }
            self.engine.RenderManager.Frame();
          }, 20);
        }
        self.mouseMoved = 0;
      }
    });
  };

  /**
   *  Implements PreviewHandler.OnResize().
   *  @see PreviewHandler.OnResize
   */
  CSMeshHandler.prototype.OnResize = function(width, height) {
    this.$view.attr('width', width);
    this.$view.attr('height', height);
    this.engine.gl.viewportWidth = width;
    this.engine.gl.viewportHeight = height;
    this.engine.RenderManager.InitCamera();
    this.Draw();
  };

  /**
   *  Implements PreviewHandler.Add().
   *  @see PreviewHandler.Add
   */
  CSMeshHandler.prototype.Add = function() {
    var self = this;
    self.engine = new CS.Engine(self.view);
    if (!self.engine.Init()) {
      // TODO: log.
      alert('WebGLCanvas failure!');
      return;
    }
    self.$parent.append(self.$view);
    self.engine.Loader.ImageURLResolver = new PreviewCSURLResolver();
    PreviewLoadOrTranscode({
      transcode: {
        uuid: self.model.assetid,
        src_format: self.model.src_format,
        format: 'application/x-crystalspace.library+xml'
      },
      type: 'ajax',
      callback: function(src, options) {
        self.engine.LoadLibraryDOM(options.data, DAMNRepository);
      }
    });
  };

  /**
   *  Implements PreviewHandler.Remove().
   *  @see PreviewHandler.Remove
   */
  CSMeshHandler.prototype.Remove = function() {
    this.$view.remove();
  };

  /**
   *  Render the WebGL canvas.
   */
  CSMeshHandler.prototype.Draw = function() {
    this.engine.RenderManager.Frame();
  };

  /**
   *  Construct a Pseudo3DMeshHandler PreviewHandler.
   *  This PreviewHandler class displays a 3D mesh as a series of pre-rendered
   *  2D images and supports basic rotation interactivity.
   */
  function Pseudo3DMeshHandler() {
    this.$view = $('<img>');
    this.view = this.$view.get(0);
    this.view.style.cursor = 'move';
    this.curRot = 1;
    this.imagesLoaded = 0;
    this.imageCache = [];
    this.errorOnce = false; // Avoid doing this.Error() a dozen times on fail.
  }
  $.extend(Pseudo3DMeshHandler.prototype, PreviewHandler.prototype);

  /**
   *  Implements PreviewHandler.OnInit().
   *  @see PreviewHandler.OnInit
   */
  Pseudo3DMeshHandler.prototype.OnInit = function() {
    var self = this;
    self.numRot = self.model.rotations ? self.model.rotations : 12;
    self.controller.Extend(Controllers.InteractiveDrag).Bind({
      'drag-start.Pseudo3DMeshHandler': function(event) {
        event.previewtarget.startPos = [event.pageX, event.pageY];
        event.previewtarget.curSegs = [0,0];
      },
      'drag.Pseudo3DMeshHandler': function(event) {
        var newSegs = [ event.previewtarget.$parent.width() / (event.previewtarget.numRot / 2), event.previewtarget.$parent.height() ];
        newSegs[2] = newSegs[0] = Math.round((event.pageX - event.previewtarget.startPos[0]) / newSegs[0]);
        newSegs[3] = newSegs[1] = Math.round((event.pageY - event.previewtarget.startPos[1]) / newSegs[1]);
        while (newSegs[0] > event.previewtarget.curSegs[0]) {
          event.previewtarget.Rotate(-1);
          newSegs[0]--;
        }
        while (newSegs[0] < event.previewtarget.curSegs[0]) {
          event.previewtarget.Rotate(1);
          newSegs[0]++;
        }
        event.previewtarget.curSegs = [newSegs[2],newSegs[3]];
      }
    });
  };

  /**
   *  Implements PreviewHandler.OnResize().
   *  @see PreviewHandler.OnResize
   */
  Pseudo3DMeshHandler.prototype.OnResize = function(width, height) {
    this.$view.width(width);
    this.$view.height(height);
  };

  /**
   *  Implements PreviewHandler.OnError().
   *  @see PreviewHandler.OnError
   */
  Pseudo3DMeshHandler.prototype.OnError = function(message) {
    this.$view.attr('src', PreviewBrokenFileIconSrc());
    this.$view.attr('title', message);
  };

  /**
   *  Implements PreviewHandler.Add().
   *  @see PreviewHandler.Add
   */
  Pseudo3DMeshHandler.prototype.Add = function() {
    var self = this;
    self.$view.width(self.$parent.width());
    self.$view.height(self.$parent.height());
    self.$parent.append(self.$view);
    for (var i = 1; i <= self.numRot; i++) {
      self.imageCache[i] = new Image();
      var angle = (((-Math.PI*2)/self.numRot)*i).toFixed(2);
      PreviewLoadOrTranscode({
        transcode: {
          uuid: self.model.assetid,
          format: 'image/png',
          src_format: self.model.src_format,
          sizex: self.model.sizex,
          sizey: self.model.sizey,
          angley: angle
        },
        index: i,
        type: 'image',
        callback: function(src, options) {
          self.imagesLoaded++;
          if (options.index == self.curRot) {
            self.$view.attr('src', src);
          }
          self.imageCache[options.index].src = src;
        },
        error: function(msg) {
          if (!self.errorOnce) {
            self.Error(msg);
            self.errorOnce = true;
          }
        }
      });
    }
  };

  /**
   *  Implements PreviewHandler.Remove().
   *  @see PreviewHandler.Remove
   */
  Pseudo3DMeshHandler.prototype.Remove = function() {
    this.$view.remove();
  };

  /**
   *  Rotate the preview.
   */
  Pseudo3DMeshHandler.prototype.Rotate = function(amount) {
    this.curRot += amount;
    if (this.curRot > this.numRot) {
      this.curRot = 1;
    }
    else if (this.curRot < 1) {
      this.curRot = this.numRot;
    }
    this.$view.attr('src', this.imageCache[this.curRot].src);
  };

  /**
   *  Construct a MeshHandler PreviewHandler.
   *  This is a multi-purpose wrapper for 3D mesh type previews. If
   *  model.lazy has been set, an image preview will be displayed first, and
   *  the 3D version will only be loaded after a click. When generating a 3D
   *  PreviewHandler, this class decides if the current system can support
   *  WebGL or not and loads the appropriate display handler.
   */
  function MeshHandler() { }
  $.extend(MeshHandler.prototype, ProxyHandler.prototype);

  /**
   *  Implements PreviewHandler.OnInit(). If model.lazy is true, a click
   *  controller is added that loads the 3D version.
   *  @see PreviewHandler.OnInit
   */
  MeshHandler.prototype.OnInit = function() {
    if (this.model.lazy) {
      this.controller.Extend(Controllers.click).Bind({
        'click.LazyClick': function(event) {
          event.handler.Remove();
          event.handler.$parent.empty();
          event.handler.proxyhandler = event.handler.Get3DHandler();
          event.handler.Add();
          event.handler.controller.Unbind('click.LazyClick'); // Once.
        }
      });
    }
  };

  /**
   *  Implements ProxyHandler.GetProxyHandler().
   *  @see ProxyHandler.GetProxyHandler
   */
  MeshHandler.prototype.GetProxyHandler = function() {
    var handler = false;
    if (this.model.lazy) {
      handler = CreatePreviewHandler('Image', this.model, this.$parent);
      handler.$parent.append('<span class="click-to-load">Click to load preview</span>');
    }
    else {
      handler = this.Get3DHandler();
    }
    return handler;
  };

  /**
   *  This function decides if the current system can support WebGL, creates a
   *  CSMeshHandler PreviewHandler if so, or a Pseudo3DMeshHandler if not.
   *  @return A PreviewHandler that handles 3D meshes.
   */
  MeshHandler.prototype.Get3DHandler = function() {
    var type = (CS && CS.hasWebGLSupport) ? 'CSMeshHandler' : 'Pseudo3DMeshHandler';
    return CreatePreviewHandler(type, this.model, this.$parent);
  };

  /**
   *  Construct an AudioHandler PreviewHandler.
   */
  function AudioHandler() {
    this.$view = $('<audio>').attr('controls', 'controls');
    this.view = this.$view.get(0);
  }
  $.extend(AudioHandler.prototype, PreviewHandler.prototype);

  /**
   *  Implements PreviewHandler.OnResize(). Short circuted to make it do
   *  nothing. The audio HTML element doesn't support sizing.
   *  @see PreviewHandler.OnResize
   */
  AudioHandler.prototype.OnResize = function(width, height) { };

  /**
   *  Implements PreviewHandler.Add().
   *  @see PreviewHandler.Add
   */
  AudioHandler.prototype.Add = function() {
    var self = this;
    PreviewLoadOrTranscode({
      transcode: {
        uuid: self.model.assetid,
        src_format: self.model.src_format,
        format: 'audio/ogg'
      },
      type: 'audio',
      callback: function(src, options) {
        var source = $('<source>').attr({
          'src': src,
          'type': 'audio/ogg; codecs="vorbis"'
        });
        self.$view.append(source);
        self.$parent.append(self.$view);
      },
      error: function(msg) {
        self.Error(msg);
      }
    });
  };

  /**
   *  Implements PreviewHandler.Remove().
   *  @see PreviewHandler.Remove
   */
  AudioHandler.prototype.Remove = function() {
    this.$view.remove();
  };

  /**
   *  Construct an AmbiguousHandler PreviewHandler.
   *  This class is used when the high level asset type is unknown. It first
   *  attemps to create a '3D Mesh' type preview, but if that causes an error,
   *  it uses an 'Image' type preview instead, which is universally supported
   *  for all asset types.
   */
  function AmbiguousHandler() { }
  $.extend(AmbiguousHandler.prototype, ProxyHandler.prototype);

  /**
   *  Implements PreviewHandler.OnError().
   *  @see PreviewHandler.OnError, AmbiguousHandler
   */
  AmbiguousHandler.prototype.OnError = function(message) {
    this.Remove();
    this.$parent.empty();
    this.proxyhandler = CreatePreviewHandler('Image', this.model, this.$parent);
    this.Add();
  };

  /**
   *  Implements ProxyHandler.GetProxyHandler().
   *  @see ProxyHandler.GetProxyHandler, AmbiguousHandler
   */
  AmbiguousHandler.prototype.GetProxyHandler = function() {
    return CreatePreviewHandler('3D Mesh', this.model, this.$parent);
  };

  var methods = {

    init : function(userdata) {

      // Create data model.
      var data = this.data();
      var model = {};                 // model =
      $.extend(model, defaults);      //  default options
      $.extend(model, data);          //  + associated element data
      if (userdata) {                 //  + user options
        $.extend(model, userdata);
      }

      // Create and initialize a PreviewHandler object.
      var type = model.imageonly ? 'Image' : (model.type ? model.type : 'None');

      // Add the display if this has not already been done.
      if (!data.DAMNPreview) {
        var handler = CreatePreviewHandler(type, model, this);
        handler.Add();
        this.data('DAMNPreview', handler);
      }

      return this;
    },

    size : function(dimensions) {
      // Resize event.
      var $this = $(this);
      var handler = $this.data('DAMNPreview');
      handler.Resize(dimensions.width, dimensions.height);
      return this;
    },

    destroy : function() {
      // Destroy event.
      var handler = this.data('DAMNPreview');
      handler.Remove();
      this.removeData('DAMNPreview');
      this.empty();
      return this;
    }

  };

  $.fn.DAMNPreview = function(method) {
    if (methods[method]) {
      return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
    }
    else if (typeof method === 'object' || !method) {
      return methods.init.apply(this, arguments);
    }
    else {
      $.error('Method ' +  method + ' does not exist on jQuery.DAMNPreview');
      return false;
    }
  };

})(jQuery);
